#region Copyright & License

//
// Copyright 2004-2005 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#endregion

using System;
using System.Globalization;
using System.Text;
using log4net.Core;
using log4net.Util;

namespace log4net.Appender {
  /// <summary>
  /// Appends logging events to the terminal using ANSI color escape sequences.
  /// </summary>
  /// <remarks>
  /// <para>
  /// AnsiColorTerminalAppender appends log events to the standard output stream
  /// or the error output stream using a layout specified by the 
  /// user. It also allows the color of a specific level of message to be set.
  /// </para>
  /// <note>
  /// This appender expects the terminal to understand the VT100 control set 
  /// in order to interpret the color codes. If the terminal or console does not
  /// understand the control codes the behavior is not defined.
  /// </note>
  /// <para>
  /// By default, all output is written to the console's standard output stream.
  /// The <see cref="Target"/> property can be set to direct the output to the
  /// error stream.
  /// </para>
  /// <para>
  /// NOTE: This appender writes each message to the <c>System.Console.Out</c> or 
  /// <c>System.Console.Error</c> that is set at the time the event is appended.
  /// Therefore it is possible to programmatically redirect the output of this appender 
  /// (for example NUnit does this to capture program output). While this is the desired
  /// behavior of this appender it may have security implications in your application. 
  /// </para>
  /// <para>
  /// When configuring the ANSI colored terminal appender, a mapping should be
  /// specified to map a logging level to a color. For example:
  /// </para>
  /// <code lang="XML" escaped="true">
  /// <mapping>
  /// 	<level value="ERROR" />
  /// 	<foreColor value="White" />
  /// 	<backColor value="Red" />
  ///     <attributes value="Bright,Underscore" />
  /// </mapping>
  /// <mapping>
  /// 	<level value="DEBUG" />
  /// 	<backColor value="Green" />
  /// </mapping>
  /// </code>
  /// <para>
  /// The Level is the standard log4net logging level and ForeColor and BackColor can be any
  /// of the following values:
  /// <list type="bullet">
  /// <item><term>Blue</term><description></description></item>
  /// <item><term>Green</term><description></description></item>
  /// <item><term>Red</term><description></description></item>
  /// <item><term>White</term><description></description></item>
  /// <item><term>Yellow</term><description></description></item>
  /// <item><term>Purple</term><description></description></item>
  /// <item><term>Cyan</term><description></description></item>
  /// </list>
  /// These color values cannot be combined together to make new colors.
  /// </para>
  /// <para>
  /// The attributes can be any combination of the following:
  /// <list type="bullet">
  /// <item><term>Bright</term><description>foreground is brighter</description></item>
  /// <item><term>Dim</term><description>foreground is dimmer</description></item>
  /// <item><term>Underscore</term><description>message is underlined</description></item>
  /// <item><term>Blink</term><description>foreground is blinking (does not work on all terminals)</description></item>
  /// <item><term>Reverse</term><description>foreground and background are reversed</description></item>
  /// <item><term>Hidden</term><description>output is hidden</description></item>
  /// <item><term>Strikethrough</term><description>message has a line through it</description></item>
  /// </list>
  /// While any of these attributes may be combined together not all combinations
  /// work well together, for example setting both <i>Bright</i> and <i>Dim</i> attributes makes
  /// no sense.
  /// </para>
  /// </remarks>
  /// <author>Patrick Wagstrom</author>
  /// <author>Nicko Cadell</author>
  public class AnsiColorTerminalAppender : AppenderSkeleton {
    #region Colors Enum

    #region AnsiAttributes enum

    /// <summary>
    /// The enum of possible display attributes
    /// </summary>
    /// <remarks>
    /// <para>
    /// The following flags can be combined together to
    /// form the ANSI color attributes.
    /// </para>
    /// </remarks>
    /// <seealso cref="AnsiColorTerminalAppender" />
    [Flags]
    public enum AnsiAttributes {
      /// <summary>
      /// text is bright
      /// </summary>
      Bright = 1,
      /// <summary>
      /// text is dim
      /// </summary>
      Dim = 2,

      /// <summary>
      /// text is underlined
      /// </summary>
      Underscore = 4,

      /// <summary>
      /// text is blinking
      /// </summary>
      /// <remarks>
      /// Not all terminals support this attribute
      /// </remarks>
      Blink = 8,

      /// <summary>
      /// text and background colors are reversed
      /// </summary>
      Reverse = 16,

      /// <summary>
      /// text is hidden
      /// </summary>
      Hidden = 32,

      /// <summary>
      /// text is displayed with a strikethrough
      /// </summary>
      Strikethrough = 64
    }

    #endregion

    #region AnsiColor enum

    /// <summary>
    /// The enum of possible foreground or background color values for 
    /// use with the color mapping method
    /// </summary>
    /// <remarks>
    /// <para>
    /// The output can be in one for the following ANSI colors.
    /// </para>
    /// </remarks>
    /// <seealso cref="AnsiColorTerminalAppender" />
    public enum AnsiColor {
      /// <summary>
      /// color is black
      /// </summary>
      Black = 0,

      /// <summary>
      /// color is red
      /// </summary>
      Red = 1,

      /// <summary>
      /// color is green
      /// </summary>
      Green = 2,

      /// <summary>
      /// color is yellow
      /// </summary>
      Yellow = 3,

      /// <summary>
      /// color is blue
      /// </summary>
      Blue = 4,

      /// <summary>
      /// color is magenta
      /// </summary>
      Magenta = 5,

      /// <summary>
      /// color is cyan
      /// </summary>
      Cyan = 6,

      /// <summary>
      /// color is white
      /// </summary>
      White = 7
    }

    #endregion

    #endregion

    #region Public Instance Constructors

    #endregion Public Instance Constructors

    #region Public Instance Properties

    /// <summary>
    /// Target is the value of the console output stream.
    /// </summary>
    /// <value>
    /// Target is the value of the console output stream.
    /// This is either <c>"Console.Out"</c> or <c>"Console.Error"</c>.
    /// </value>
    /// <remarks>
    /// <para>
    /// Target is the value of the console output stream.
    /// This is either <c>"Console.Out"</c> or <c>"Console.Error"</c>.
    /// </para>
    /// </remarks>
    public virtual string Target {
      get { return m_writeToErrorStream ? ConsoleError : ConsoleOut; }
      set {
        string trimmedTargetName = value.Trim();

        if (string.Compare(ConsoleError, trimmedTargetName, true, CultureInfo.InvariantCulture) == 0)
          m_writeToErrorStream = true;
        else
          m_writeToErrorStream = false;
      }
    }

    /// <summary>
    /// Add a mapping of level to color
    /// </summary>
    /// <param name="mapping">The mapping to add</param>
    /// <remarks>
    /// <para>
    /// Add a <see cref="LevelColors"/> mapping to this appender.
    /// Each mapping defines the foreground and background colours
    /// for a level.
    /// </para>
    /// </remarks>
    public void AddMapping(LevelColors mapping) {
      m_levelMapping.Add(mapping);
    }

    #endregion Public Instance Properties

    #region Override implementation of AppenderSkeleton

    /// <summary>
    /// This appender requires a <see cref="Layout"/> to be set.
    /// </summary>
    /// <value><c>true</c></value>
    /// <remarks>
    /// <para>
    /// This appender requires a <see cref="Layout"/> to be set.
    /// </para>
    /// </remarks>
    protected override bool RequiresLayout {
      get { return true; }
    }

    /// <summary>
    /// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent)"/> method.
    /// </summary>
    /// <param name="loggingEvent">The event to log.</param>
    /// <remarks>
    /// <para>
    /// Writes the event to the console.
    /// </para>
    /// <para>
    /// The format of the output will depend on the appender's layout.
    /// </para>
    /// </remarks>
    protected override void Append(LoggingEvent loggingEvent) {
      string loggingMessage = RenderLoggingEvent(loggingEvent);

      // see if there is a specified lookup.
      var levelColors = m_levelMapping.Lookup(loggingEvent.Level) as LevelColors;
      if (levelColors != null) // Prepend the Ansi Color code
        loggingMessage = levelColors.CombinedColor + loggingMessage;

      // on most terminals there are weird effects if we don't clear the background color
      // before the new line.  This checks to see if it ends with a newline, and if
      // so, inserts the clear codes before the newline, otherwise the clear codes
      // are inserted afterwards.
      if (loggingMessage.Length > 1)
        if (loggingMessage.EndsWith("\r\n") || loggingMessage.EndsWith("\n\r"))
          loggingMessage = loggingMessage.Insert(loggingMessage.Length - 2, PostEventCodes);
        else if (loggingMessage.EndsWith("\n") || loggingMessage.EndsWith("\r"))
          loggingMessage = loggingMessage.Insert(loggingMessage.Length - 1, PostEventCodes);
        else
          loggingMessage = loggingMessage + PostEventCodes;
      else if (loggingMessage[0] == '\n' || loggingMessage[0] == '\r')
        loggingMessage = PostEventCodes + loggingMessage;
      else
        loggingMessage = loggingMessage + PostEventCodes;

#if NETCF
  // Write to the output stream
			Console.Write(loggingMessage);
#else
      if (m_writeToErrorStream) // Write to the error stream
        Console.Error.Write(loggingMessage);
      else // Write to the output stream
        Console.Write(loggingMessage);
#endif
    }

    /// <summary>
    /// Initialize the options for this appender
    /// </summary>
    /// <remarks>
    /// <para>
    /// Initialize the level to color mappings set on this appender.
    /// </para>
    /// </remarks>
    public override void ActivateOptions() {
      base.ActivateOptions();
      m_levelMapping.ActivateOptions();
    }

    #endregion Override implementation of AppenderSkeleton

    #region Public Static Fields

    /// <summary>
    /// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
    /// standard error output stream.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
    /// standard error output stream.
    /// </para>
    /// </remarks>
    public const string ConsoleError = "Console.Error";

    /// <summary>
    /// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
    /// standard output stream.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
    /// standard output stream.
    /// </para>
    /// </remarks>
    public const string ConsoleOut = "Console.Out";

    #endregion Public Static Fields

    #region Private Instances Fields

    /// <summary>
    /// Ansi code to reset terminal
    /// </summary>
    const string PostEventCodes = "\x1b[0m";

    /// <summary>
    /// Mapping from level object to color value
    /// </summary>
    readonly LevelMapping m_levelMapping = new LevelMapping();

    /// <summary>
    /// Flag to write output to the error stream rather than the standard output stream
    /// </summary>
    bool m_writeToErrorStream;

    #endregion Private Instances Fields

    #region LevelColors LevelMapping Entry

    /// <summary>
    /// A class to act as a mapping between the level that a logging call is made at and
    /// the color it should be displayed as.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Defines the mapping between a level and the color it should be displayed in.
    /// </para>
    /// </remarks>
    public class LevelColors : LevelMappingEntry {
      AnsiAttributes m_attributes;
      AnsiColor m_backColor;
      string m_combinedColor = "";
      AnsiColor m_foreColor;

      /// <summary>
      /// The mapped foreground color for the specified level
      /// </summary>
      /// <remarks>
      /// <para>
      /// Required property.
      /// The mapped foreground color for the specified level
      /// </para>
      /// </remarks>
      public AnsiColor ForeColor {
        get { return m_foreColor; }
        set { m_foreColor = value; }
      }

      /// <summary>
      /// The mapped background color for the specified level
      /// </summary>
      /// <remarks>
      /// <para>
      /// Required property.
      /// The mapped background color for the specified level
      /// </para>
      /// </remarks>
      public AnsiColor BackColor {
        get { return m_backColor; }
        set { m_backColor = value; }
      }

      /// <summary>
      /// The color attributes for the specified level
      /// </summary>
      /// <remarks>
      /// <para>
      /// Required property.
      /// The color attributes for the specified level
      /// </para>
      /// </remarks>
      public AnsiAttributes Attributes {
        get { return m_attributes; }
        set { m_attributes = value; }
      }

      /// <summary>
      /// The combined <see cref="ForeColor"/>, <see cref="BackColor"/> and
      /// <see cref="Attributes"/> suitable for setting the ansi terminal color.
      /// </summary>
      internal string CombinedColor {
        get { return m_combinedColor; }
      }

      /// <summary>
      /// Initialize the options for the object
      /// </summary>
      /// <remarks>
      /// <para>
      /// Combine the <see cref="ForeColor"/> and <see cref="BackColor"/> together
      /// and append the attributes.
      /// </para>
      /// </remarks>
      public override void ActivateOptions() {
        base.ActivateOptions();

        var buf = new StringBuilder();

        // Reset any existing codes
        buf.Append("\x1b[0;");

        // set the foreground color
        buf.Append(30 + (int) m_foreColor);
        buf.Append(';');

        // set the background color
        buf.Append(40 + (int) m_backColor);

        // set the attributes
        if ((m_attributes & AnsiAttributes.Bright) > 0)
          buf.Append(";1");
        if ((m_attributes & AnsiAttributes.Dim) > 0)
          buf.Append(";2");
        if ((m_attributes & AnsiAttributes.Underscore) > 0)
          buf.Append(";4");
        if ((m_attributes & AnsiAttributes.Blink) > 0)
          buf.Append(";5");
        if ((m_attributes & AnsiAttributes.Reverse) > 0)
          buf.Append(";7");
        if ((m_attributes & AnsiAttributes.Hidden) > 0)
          buf.Append(";8");
        if ((m_attributes & AnsiAttributes.Strikethrough) > 0)
          buf.Append(";9");

        buf.Append('m');

        m_combinedColor = buf.ToString();
      }
    }

    #endregion // LevelColors LevelMapping Entry
  }
}